﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Diagnostics;
using System.Threading;
using Common.Logging;
using NPMonitor.Interfaces;
using NPMonitor.Interfaces.Internal;
using NPMonitor.DataModel;
using NPMonitor.DataModel.Internal;
using NPMonitor.InternalJobs;

namespace NPMonitor.Implementation.Internal
{
	/// <summary>
	/// This is the default job executor.  It is used to manage the execution of a single instantiated job.
	/// </summary>
	internal class DefaultJobExecutor : IJobExecutor
	{
        protected IJobManager jobManager;
        protected ILog logger;
        protected JobData jobData;

        protected virtual List<JobInstance> GetExecuteJobList(AppDomain domain)
        {
            List<JobInstance> list = new List<JobInstance>();

            JobInstance job = new JobInstance()
            {
                Job = JobFileLoader.NewJobInstance(domain, jobData.TaskId.Value),
                WorkflowJob = null
            };

            list.Add(job);

            return list;
        }

		/// <summary>
		/// Executes the job specified in the job context and updates the jobstore accordingly.
		/// </summary>
		/// <param name="jobContext">The job context.</param>
        public void ExecuteJob(JobContext jobContext)
        {
            jobManager = jobContext.JobManager;
            jobData = jobContext.JobData;
            logger = jobContext.JobManager.Logger;

            ValidateJobContext(jobContext);

            ExecuteSingleJob(jobData);

            if (jobData.DeleteWhenDone)
            {
                try
                {
                    jobManager.JobStore.DeleteJob(jobData);
                }
                catch (Exception ex)
                {
                    logger.Error("Could not delete job that was set to 'DeleteWhenDone'.", ex);
                }
            }
        }

        private void ValidateJobContext(JobContext jobContext)
        {
            if (jobContext == null)
            {
                throw new ArgumentNullException("jobContext");
            }

            if (jobContext.JobData == null)
            {
                throw new ArgumentNullException("jobContext.JobData");
            }

            if (jobContext.JobManager == null)
            {
                throw new ArgumentNullException("jobContext.JobManager");
            }
        }

        private void ExecuteSingleJob(JobData jobData)
        {
            AppDomain execDomain = null;
            List<JobExecutionResult> returnValueList = new List<JobExecutionResult>();
            Guid historyUniqueId = Helpers.Utils.GetNewGuid();

            JobInstance lastJobInstance = null;
            bool existFailedJob = false;    // if there is job executions failed

            try
            {
                Debug.WriteLine(Helpers.Utils.CurrentServerUtcTime.ToString("dd/mm/yyyy HH:mm:ss:fffffff") + " : " + jobData.Id + " start execution.");

                jobData.LastStartTime = Helpers.Utils.CurrentServerUtcTime;
                jobData.LastEndTime = null;
                jobData.LastErrorMessage = null;
                jobData.Status = JobStatus.Executing;
                jobManager.JobStore.UpdateJob(jobData);

                execDomain = JobFileLoader.NewJobExecutionDomain();

                var jobList = GetExecuteJobList(execDomain);

                foreach (var job in jobList)
                {
                    lastJobInstance = job;

                    JobData cloneJobData = CloneJobDataByJobInstance(jobData, job);

                    JobExecutionResult returnValue = null;
                    
                    try
                    {
                        returnValue = job.Job.Execute(cloneJobData.LastStartTime.ToFormatString("yyyy-MM-dd HH.mm.ss"), historyUniqueId);

                        if (returnValue == null)
                        {
                            throw new ArgumentException("Execute(string jobData, string recoveryData) for job '" + jobData.Id + "' returned null.");
                        }
                    }
                    catch (Exception innerEx)
                    {
                        returnValue = new JobExecutionResult()
                        {
                            ErrorMessage = innerEx.Message,
                            ResultStatus = JobResultStatus.Fail
                        };
                    }

                    if (existFailedJob == false && returnValue.ResultStatus == JobResultStatus.Fail)
                    {
                        existFailedJob = true;
                    }

                    returnValueList.Add(returnValue);

                    switch (returnValue.ResultStatus)
                    {
                        case JobResultStatus.Success:
                            cloneJobData.Status = JobStatus.Done;
                            break;
                        case JobResultStatus.FailAutoRetry:
                            cloneJobData.Status = JobStatus.FailAutoRetry;
                            break;
                        default:
                            cloneJobData.Status = JobStatus.Fail;
                            break;
                    }

                    cloneJobData.LastEndTime = Helpers.Utils.CurrentServerUtcTime;
                    cloneJobData.ErrorCode = returnValue.ErrorCode;
                    cloneJobData.LastErrorMessage = (!string.IsNullOrEmpty(returnValue.ErrorMessage) ? returnValue.ErrorMessage : null);

                    if (returnValue.MetaData != null && jobData.MetaData != returnValue.MetaData)
                    {
                        cloneJobData.MetaData = returnValue.MetaData;
                    }

                    InsertHistory(cloneJobData,historyUniqueId);

                    if (returnValue.ResultStatus != JobResultStatus.Success 
                        && job.WorkflowJob != null 
                        && job.WorkflowJob.JobFailureAction == WorkflowJobFailureAction.Stop)
                    {
                        break;
                    }

                    Thread.Sleep(TimeSpan.FromSeconds(5));  // Wait 5 seconds for execution disposing
                }

                int failCount = returnValueList.Where(t => t.ResultStatus == JobResultStatus.Fail).Count();
                jobData.Status = failCount > 0 ? JobStatus.Fail : JobStatus.Done;
                jobData.LastEndTime = Helpers.Utils.CurrentServerUtcTime;
            }
            catch (ThreadAbortException ex)
            {
                if ("TimeOut".Equals(ex.ExceptionState))
                {
                    if (existFailedJob == true)
                    {
                        jobData.Status = JobStatus.Fail;
                    }
                    else
                    {
                        jobData.Status = JobStatus.ExecutionTimeout;
                    }
                }
                else
                {
                    jobData.LastErrorMessage = "Unhandled ThreadAbout Exception." + ex.Message;
                    jobData.Status = JobStatus.Fail;
                }

                jobData.LastEndTime = Helpers.Utils.CurrentServerUtcTime;

                // Recover timeout status for history recording
                JobData lastJobData = CloneJobDataByJobInstance(jobData, lastJobInstance);
                if ("TimeOut".Equals(ex.ExceptionState))
                {
                    lastJobData.Status = JobStatus.ExecutionTimeout;
                }

                InsertHistory(lastJobData, historyUniqueId);
            }
            catch (Exception ex)
            {
                jobData.Status = JobStatus.Fail;
                jobData.LastErrorMessage = Helpers.Utils.GetExceptionMessage(ex);
                jobData.LastEndTime = Helpers.Utils.CurrentServerUtcTime;

                InsertHistory(jobData,historyUniqueId);
            }
            finally
            {
                // Dispose execute domain
                if (execDomain != null)
                {
                    AppDomain.Unload(execDomain);
                    execDomain = null;
                }

                RescheduleJobAfterExecution(jobData);

                string nextTime = jobData.NextStartTime.ToNullFilteredString();

                // retrieve current job status, keep status unchanged when it is paused
                JobData tjobData = StoreManager.Current.JobStore.GetJobs(jobIds: new long[] { jobData.Id }).FirstOrDefault();
                if (tjobData.Status == JobStatus.Paused)
                {
                    jobData.Status = JobStatus.Paused;
                }

                StoreManager.Current.JobStore.UpdateJob(jobData);
            }
        }

        /// <summary>
        /// Caculate and store job status after job execution
        /// </summary>
        /// <param name="jobData"></param>
        private void RescheduleJobAfterExecution(JobData jobData)
        {
            // caculate retry times by status
            if (jobData.Status == JobStatus.Fail)
            {
                jobData.NextStartTime = null;

                if ((jobData.RetryTimes ?? 0) < (jobData.MaxRetry ?? 0))
                {
                    jobData.RetryTimes = ((jobData.RetryTimes ?? 0) + 1);

                    if (jobData.AutoRetry == true)
                    {
                        jobData.Status = JobStatus.FailAutoRetry;
                    }
                    else if (jobData.Retry == true)
                    {
                        jobData.Status = JobStatus.FailRetry;
                    }
                }
            }

            DateTime nextOccurrentReferTime = (jobData.LastEndTime ?? jobData.LastStartTime) ?? jobData.JobSchedule.StartDateTime;

            // recaculate job status
            switch (jobData.Status)
            {
                case JobStatus.FailRetry:
                    if (jobData.Schedule != null)
                    {
                        jobData.NextStartTime = jobData.JobSchedule.GetNextOccurrence(nextOccurrentReferTime);
                    }
                    break;
                case JobStatus.Done:
                    jobData.RetryTimes = 0; // reset retry times when success
                    if (jobData.Schedule != null)
                    {
                        jobData.Status = JobStatus.Scheduled;
                        jobData.NextStartTime = jobData.JobSchedule.GetNextOccurrence(nextOccurrentReferTime);
                    }
                    break;
                case JobStatus.ExecutionTimeout:
                    jobData.RetryTimes = 0; // reset retry times when success
                    if (jobData.Schedule != null)
                    {
                        jobData.NextStartTime = jobData.JobSchedule.GetNextOccurrence(nextOccurrentReferTime);
                    }
                    break;
                case JobStatus.FailAutoRetry:
                    jobData.Status = JobStatus.Ready;
                    break;
            }
        }

        private static JobData CloneJobDataByJobInstance(JobData jobData, JobInstance job)
        {
            JobData cloneJobData = Helpers.Utils.CloneObject(jobData);

            if (job != null)
            {
                if (job.WorkflowJob != null)
                {
                    cloneJobData.JobId = job.WorkflowJob.JobId;
                    cloneJobData.Name = job.WorkflowJob.JobName;
                    cloneJobData.LastStartTime = Helpers.Utils.CurrentServerUtcTime;
                }
                else
                {
                    cloneJobData.Name = jobData.JobWorkflowName;
                }
            }

            return cloneJobData;
        }

        private void InsertHistory(JobData jobData, Guid historyUniqueId)
        {
            JobExecutionHistory history = null;

            try
            {
                if (!jobData.SuppressHistory && jobData.Status != JobStatus.Ready) //Status check added for the extreme case where the thread was aborted
                {
                    history = jobManager.JobStore.CreateJobExecutionHistory(jobData, historyUniqueId);
                }
            }
            catch (Exception ex)
            {
                logger.Error("Could not create job execution history record.", ex);
            }

            if (jobData.Status != JobStatus.Done)
            {
                jobManager.JobStore.CreateAlert(jobData.Id, (history != null ? (long?)history.Id : null), jobData.LastErrorMessage);
            }

            Debug.WriteLine(Helpers.Utils.CurrentServerUtcTime.ToString("dd/mm/yyyy HH:mm:ss:fffffff") + " : " + jobData.Id + " end execution.");
        }
    }
}
